package com.gitee.l0km.codegen.thrift;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import com.facebook.swift.codec.metadata.ReflectionHelper;
import com.gitee.l0km.aocache.annotations.AoCacheable;
import com.gitee.l0km.codegen.thrift.CxxTypeMeta.ThriftProtocolType;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;

import static com.facebook.swift.codec.metadata.ReflectionHelper.getIterableType;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapKeyType;
import static com.facebook.swift.codec.metadata.ReflectionHelper.getMapValueType;
import static com.google.common.base.Preconditions.*;

/**
 * 定义java转cxx的类型
 * @author guyadong
 *
 */
public class CxxType {
	public static final CxxType BOOL = new CxxType(boolean.class, CxxTypeMeta.BOOL, null);
    public static final CxxType BYTE = new CxxType(byte.class, CxxTypeMeta.BYTE, null);
    public static final CxxType DOUBLE = new CxxType(double.class, CxxTypeMeta.DOUBLE, null);
    public static final CxxType I16 = new CxxType(short.class, CxxTypeMeta.I16, null);
    public static final CxxType I32 = new CxxType(int.class, CxxTypeMeta.I32, null);
    public static final CxxType I64 = new CxxType(long.class, CxxTypeMeta.I64, null);
    public static final CxxType STRING = new CxxType(String.class, CxxTypeMeta.STRING, null);
    public static final CxxType BINARY = new CxxType(ByteBuffer.class, CxxTypeMeta.BINARY, null);
    public static final CxxType VOID = new CxxType(void.class, CxxTypeMeta.VOID, null);
    
    private static final ConcurrentMap<Class<?>, CxxType> manualTypes = new ConcurrentHashMap<>();
	public static final ImmutableMap<Class<?>, CxxType> THRIFT_BUILTIN_CXX_TYPES = ImmutableMap.<Class<?>, CxxType>builder()
			.put(boolean.class, BOOL)
			.put(byte.class, BYTE)
			.put(double.class, DOUBLE)
			.put(short.class, I16)
			.put(int.class, I32)
			.put(long.class, I64)
			.put(String.class, STRING)
			.put(ByteBuffer.class, BINARY)
			.put(void.class, VOID)
			.put(Boolean.class, new CxxType(Boolean.class, CxxTypeMeta.BOOL, null))
			.put(Byte.class, new CxxType(Byte.class, CxxTypeMeta.BYTE, null))
			.put(Double.class, new CxxType(Double.class, CxxTypeMeta.DOUBLE, null))
			.put(Short.class, new CxxType(Short.class, CxxTypeMeta.I16, null))
			.put(Integer.class, new CxxType(Integer.class, CxxTypeMeta.I32, null))
			.put(Long.class, new CxxType(Long.class, CxxTypeMeta.I64, null))
			.put(Void.class, new CxxType(Void.class, CxxTypeMeta.VOID, null))
			.build();
	private static final CxxTypeMeta CAST_VECTOR= CxxTypeMeta.BINARY.cast(null,CxxTypeMeta.BYTE,true);
	private static final CxxTypeMeta CAST_DATE= CxxTypeMeta.I64.cast("std::tm");
	private static final CxxTypeMeta CAST_FLOAT= CxxTypeMeta.DOUBLE.cast("float");
	public static final ImmutableMap<Class<?>, CxxType> THRIFT_CAST_CXX_TYPES = ImmutableMap.<Class<?>, CxxType>builder()
			.put(byte[].class,new CxxType(byte[].class,CxxTypeMeta.BINARY,null))
			.put(Date.class,new CxxType(Date.class, CxxTypeMeta.I64,CxxType.CAST_DATE))
			.put(java.sql.Date.class,new CxxType(java.sql.Date.class, CxxTypeMeta.I64,CxxType.CAST_DATE))
			.put(java.sql.Time.class,new CxxType(java.sql.Time.class, CxxTypeMeta.I64,CxxType.CAST_DATE))
			.put(float.class,new CxxType(float.class, CxxTypeMeta.DOUBLE,CxxType.CAST_FLOAT))
			.put(Float.class,new CxxType(Float.class, CxxTypeMeta.DOUBLE,CxxType.CAST_FLOAT))
			.put(char.class,new CxxType(char.class, CxxTypeMeta.I16,null))
			.put(Character.class,new CxxType(Character.class, CxxTypeMeta.I16,null))
			.put(URI.class,new CxxType(URI.class, CxxTypeMeta.STRING,null))
			.put(URL.class,new CxxType(URL.class, CxxTypeMeta.STRING,null))
			.build();	

	private final Type javaType;
	/**
	 * 内部类型
	 */
	private final CxxTypeMeta stubType;
	/**
	 * 用户接口类型，为null时即为{@link #stubType}
	 */
	private final CxxTypeMeta uiType;
	public static CxxType struct(Type javaType)
    {    	
	    checkNotNull(javaType, "javaType is null");
	    checkArgument(javaType instanceof Class<?>,"javaType must be a Class");	    
	    Class<?> type = (Class<?>)javaType;
	    String stubType = "::"+CxxHelper.cxxNamespace(ThriftServiceDecoratorConfiguration.INSTANCE.getThriftClientPackage(),true) + "::" + type.getSimpleName();
	    String uiType = CxxHelper.cxxClassName((Class<?>)javaType,true);
        return new CxxType(javaType,CxxTypeMeta.struct(stubType),CxxTypeMeta.struct(uiType));
    }
    public static CxxType exception(Type javaType)
    {    	
	    checkNotNull(javaType, "javaType is null");
	    checkArgument(javaType instanceof Class<?>,"javaType must be a Class");
	    Class<?> type = (Class<?>)javaType;
	    String stubType = "::"+CxxHelper.cxxNamespace(ThriftServiceDecoratorConfiguration.INSTANCE.getThriftClientPackage(),true) + "::" + type.getSimpleName();
	    String uiType = CxxHelper.cxxClassName((Class<?>)javaType,true);
    	return new CxxType(javaType,CxxTypeMeta.struct(stubType),CxxTypeMeta.struct(uiType));
    }
    public static CxxType enumType(Type javaType)
	{
	    checkNotNull(javaType, "javaType is null");
	    checkArgument(javaType instanceof Class<?> && Enum.class.isAssignableFrom((Class<?>) javaType),
	    		"javaType (%s) must be a enum Class",javaType);
	    Class<?> type = (Class<?>)javaType;
	    String stubType = "::"+CxxHelper.cxxNamespace(ThriftServiceDecoratorConfiguration.INSTANCE.getThriftClientPackage(),true) + "::" + type.getSimpleName() + "::type";
	    String uiType = CxxHelper.cxxClassName((Class<?>)javaType,true);
	    return new CxxType(javaType,CxxTypeMeta.enumType(stubType),CxxTypeMeta.enumType(uiType));
	}

	public static <K, V> CxxType map(CxxType keyType, CxxType valueType)
    {
		checkArgument(null != keyType, "keyType is null");
		checkArgument(null != valueType, "valueType is null");

        @SuppressWarnings({ "serial", "unchecked" })
        Type javaType = new TypeToken<Map<K, V>>(){}
                .where(new TypeParameter<K>(){}, (TypeToken<K>) TypeToken.of(keyType.getJavaType()))
                .where(new TypeParameter<V>(){}, (TypeToken<V>) TypeToken.of(valueType.getJavaType()))
                .getType();
	    CxxTypeMeta uiType = null;
	    if(null != keyType.uiType){
	    	uiType = CxxTypeMeta.map(keyType.uiType, MoreObjects.firstNonNull(valueType.uiType,valueType.stubType));
	    }else if(null != valueType.uiType){
	    	uiType = CxxTypeMeta.map(MoreObjects.firstNonNull(keyType.uiType,keyType.stubType),valueType.uiType);
	    }
	    CxxTypeMeta thriftCxxType = CxxTypeMeta.map(keyType.stubType,valueType.stubType);
        return new CxxType(javaType, thriftCxxType, uiType);
    }
	public static CxxType container(ParameterizedType type )
    {
        checkNotNull(type, "type is null");
        Class<?> rawType = TypeToken.of(type).getRawType();
        Type[] actTypes = type.getActualTypeArguments();
        if(Map.class.isAssignableFrom(rawType)){
            return map(getThriftType(actTypes[0]), getThriftType(actTypes[1]));
        }else if(List.class.isAssignableFrom(rawType)){
            return list(getThriftType(actTypes[0]));
        }else if(Set.class.isAssignableFrom(rawType)){
        	return set(getThriftType(actTypes[0]));
        }
        throw new IllegalArgumentException("unsupported type, type must be one of Map,List,Set:" + type);
    }
    public static <E> CxxType set(CxxType valueType)
    {
    	checkArgument(null != valueType, "valueType is null");

        @SuppressWarnings({ "serial", "unchecked" })
        Type javaType = new TypeToken<Set<E>>(){}
                .where(new TypeParameter<E>(){}, (TypeToken<E>) TypeToken.of(valueType.getJavaType()))
                .getType();
        CxxTypeMeta uiType = CxxTypeMeta.set(valueType.uiType);
        CxxTypeMeta thriftCxxType = CxxTypeMeta.set(valueType.stubType);
        return new CxxType(javaType, thriftCxxType, uiType);
    }

    public static <E> CxxType list(CxxType valueType)
    {
        checkArgument(null != valueType, "valueType is null");

        @SuppressWarnings({ "serial", "unchecked" })
        Type javaType = new TypeToken<List<E>>(){}
                .where(new TypeParameter<E>(){}, (TypeToken<E>) TypeToken.of(valueType.getJavaType()))
                .getType();
        CxxTypeMeta uiType = CxxTypeMeta.list(valueType.uiType);
        CxxTypeMeta thriftCxxType = CxxTypeMeta.list(valueType.stubType);
        return new CxxType(javaType, thriftCxxType, uiType);
    }

    public static CxxType array(CxxType valueType)
    {
    	checkArgument(null != valueType, "valueType is null");
        Class<?> javaType = ReflectionHelper.getArrayOfType(valueType.getJavaType());
        CxxTypeMeta thriftCxxType = CxxTypeMeta.list(valueType.stubType);
        // array类型与list不同,必有转换类型
        CxxTypeMeta uiType = CxxTypeMeta.array(MoreObjects.firstNonNull(valueType.uiType, valueType.stubType));
        return new CxxType(javaType, thriftCxxType, uiType);
    }

	/**
	 * Gets the CxxType for the specified Java type.  The native Thrift type for the Java type will
	 * be inferred from the Java type, and if necessary type coercions will be applied.
	 *
	 * @param javaType
	 * @return the CxxType for the specified java type; never null
	 * @throws IllegalArgumentException if the Java Type can not be coerced to a CxxType
	 */
    @AoCacheable
	public static CxxType getThriftType(Type javaType)
	        throws IllegalArgumentException
	{
		checkArgument(javaType instanceof Class<?>|| javaType instanceof ParameterizedType,
				"invalid javaType:"  + javaType);
	    Class<?> rawType = TypeToken.of(javaType).getRawType();
	    CxxType manualType = manualTypes.get(rawType);
	    if (manualType != null) {
	        return manualType;
	    }
	    CxxType buildin = THRIFT_BUILTIN_CXX_TYPES.get(rawType);
	    if(null != buildin){
	    	return buildin;
	    }
	    CxxType uiType = THRIFT_CAST_CXX_TYPES.get(rawType);
	    if(null != uiType){
	    	return uiType;
	    }
	    
	    if (Enum.class.isAssignableFrom(rawType)) {
	        return enumType(rawType);
	    }
	    if (rawType.isArray()) {
	        Class<?> elementType = rawType.getComponentType();
	        return array(getThriftType(elementType));
	    }
	    if (Map.class.isAssignableFrom(rawType)) {
	        Type mapKeyType = getMapKeyType(javaType);
	        Type mapValueType = getMapValueType(javaType);
	        return map(getThriftType(mapKeyType), getThriftType(mapValueType));
	    }
	    if (Set.class.isAssignableFrom(rawType)) {
	        Type elementType = getIterableType(javaType);
	        return set(getThriftType(elementType));
	    }
	    if (Iterable.class.isAssignableFrom(rawType)) {
	        Type elementType = getIterableType(javaType);
	        return list(getThriftType(elementType));
	    }
	    if(Exception.class.isAssignableFrom(rawType)){
	    	return exception(rawType);
	    }
		checkArgument(Object.class != rawType,"unsupport not type Object.class");
		String pkg = rawType.getPackage().getName();
		checkArgument( !pkg.startsWith("java.") && !pkg.startsWith("javax."),"not allow type %s",javaType);
	    return struct(rawType);
	}
	public static void addThriftType(CxxType thriftType)
	{
	    manualTypes.put(TypeToken.of(thriftType.getJavaType()).getRawType(), thriftType);
	}
	/**
	 * 如果字段类型与getter/setter方法类型不同则创建字段类型自定义的CxxType实例
	 * @param uiType
	 * @param fieldType
	 * @since 2.3.5
	 */
	@AoCacheable
	public static CxxType from(Type uiType, Type fieldType) {
		checkNotNull(uiType, "uiType is null");
		if(uiType.equals(fieldType)|| fieldType == null) {
			return getThriftType(uiType);
		}
		return new CxxType(uiType, 
				getThriftType(fieldType).getStubType(), 
				getThriftType(uiType).getUiType());
	}
	private CxxType(Type javaType, CxxTypeMeta thriftCxxType, CxxTypeMeta uiType){
		this.javaType = checkNotNull(javaType, "javaType is null");
		this.stubType=checkNotNull(thriftCxxType, "thriftCxxType is null");
		this.uiType = uiType;
	}
	public CxxTypeMeta getUiType(){
		return MoreObjects.firstNonNull(uiType, stubType);
	}
	public CxxTypeMeta getStubType(){
		return stubType;
	}
	public ThriftProtocolType getProtocolType() {
		return getStubType().getProtocolType();
	}
	public Type getJavaType() {
		return javaType;
	}
    public boolean isException() {
		Class<?> rawType = TypeToken.of(javaType).getRawType();
		return Exception.class.isAssignableFrom(rawType);
	}
	public boolean isPrimitive(){
		return TypeToken.of(getJavaType()).isPrimitive();
	}
	/**
	 * @since 2.3.5
	 */
	public boolean isString(){
		return TypeToken.of(getJavaType()).getRawType().equals(String.class);
	}
	public boolean isEnum(){
		return TypeToken.of(getJavaType()).getRawType().isEnum();
	}
	public boolean isDate(){
		Type type = getJavaType();
		return type instanceof Class<?> && Date.class.isAssignableFrom((Class<?>) type);
	}
	/**
	 * 判断类型是否需要类型转换
	 */
	public boolean isNeedCast(){
		return !getUiType().equals(getStubType());
	}
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("CxxType [javaType=");
		builder.append(javaType);
		builder.append(", uiType=");
		builder.append(uiType);
		builder.append(", stubType=");
		builder.append(stubType);
		builder.append("]");
		return builder.toString();
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((uiType == null) ? 0 : uiType.hashCode());
		result = prime * result + ((javaType == null) ? 0 : javaType.hashCode());
		result = prime * result + ((stubType == null) ? 0 : stubType.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		CxxType other = (CxxType) obj;
		if (uiType == null) {
			if (other.uiType != null)
				return false;
		} else if (!uiType.equals(other.uiType))
			return false;
		if (javaType == null) {
			if (other.javaType != null)
				return false;
		} else if (!javaType.equals(other.javaType))
			return false;
		if (stubType == null) {
			if (other.stubType != null)
				return false;
		} else if (!stubType.equals(other.stubType))
			return false;
		return true;
	}
	
}
